Conversation
…nts on idle streams
|
Verified end to end against a real deployment rather than only
So the comment resets a real proxy's read timer, the idle-reset spacing is exact (each comment lands |
|
@yhxlele can you target for now only the subscriptions/listen case in the PR for now, discarding what is related to deprecated features (GET stream, EventStore..) |
|
Sure, deprecated feature related modification discarded. |
| } | ||
| _, err := fmt.Fprint(s.w, ": keepalive\n\n") |
There was a problem hiding this comment.
let's remove the keepalive to keep it matching with the example in the spec
… bare comment from the spec
The 2026-07-28 Streamable HTTP spec encourages servers to periodically emit an SSE comment line on long-lived streams, in particular the
subscriptions/listenresponse, so that idle-timeout intermediaries do not sever them. The SDK had no way to do this: the stream'sResponseWriteris private and every write goes throughdeliverLockedunderstream.mu. Servers behind a proxy ended up sending fakenotifications/resources/updatedas a heartbeat instead.This adds
StreamableHTTPOptions.StreamKeepAlive time.Duration. The response stream of asubscriptions/listenrequest gets a goroutine that writes the bare:\n\ncomment from the spec example and flushes whenever the stream has carried no bytes for that duration. Zero selectsDefaultStreamKeepAlive(30s), so servers follow the spec out of the box; a negative value disables the keep-alive. This is the same convention asMaxRequestBodyBytes. Other SSE responses are not kept alive; the standalone GET stream andEventStorepaths are untouched.Semantics:
stream.lastWriteis stamped by every write indeliverLocked; the goroutine sleeps untillastWrite + interval, so a busy stream gets no comments and a quiet one gets exactly one per interval.deliverLockedmay still need to set a 400/404 status for a SEP-2575 error, which requires uncommitted headers, so the goroutine parks on acommittedchannel that the first write closes. The acknowledgment arrives within milliseconds, so this costs nothing.stream.mu, so a comment can never interleave with an event.donechannel:hangResponsereturns, the request context is cancelled, and the listen handler unwinds and unsubscribes. A dead peer is therefore noticed within one interval rather than at the next real notification.X-Accel-Buffering: nois set on SSE POST responses, which the same spec section recommends; the header is dropped again on the JSON error-override path.Tests: a raw listen POST sees the ack and then only comments; a slow
tools/callgets no comment at all; the option resolution (zero, negative, explicit, nil options) is checked; the goroutine semantics (park, idle-reset, failed write closes the stream) are tested directly; keep-alive goroutines end with their streams; and an end-to-end test puts an idle-timeout proxy in front of the server and checks that a quiet subscription dies with the keep-alive disabled and survives with it, with the SDK client ignoring the comments.Fixes #1229